package com.crime.crimemonitorclient;

import java.text.DecimalFormat;
import java.text.FieldPosition;
import java.text.Format;
import java.text.ParsePosition;
import java.util.Calendar;

import android.content.Context;
import android.graphics.PointF;
import android.util.Pair;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;

import com.androidplot.xy.BoundaryMode;
import com.androidplot.xy.XYPlot;

public class PlotFragment
	implements OnTouchListener, GestureDetector.OnGestureListener, GestureDetector.OnDoubleTapListener,
	OnCrimeObservationsLoadedListener
{
	private final XYPlot mPlot;
	private final GestureDetector mGestureDetector;
	
	private float lastX;
	
	private long mMinTime;
	private long mMaxTime;
	
	private TimeSeriesManager mTimeSeriesManager;
	private YearSeriesManager mYearSeriesManager = new YearSeriesManager();
	private MonthSeriesManager mMonthSeriesManager = new MonthSeriesManager();
	private DaySeriesManager mDaySeriesManager = new DaySeriesManager();
	private HourSeriesManager mHourSeriesManager = new HourSeriesManager();
	
	private final Context mContext;
	
	private final static float MIN_FINGER_DISTANCE_FOR_ZOOM = 0.5f;
	
	private final CrimeObservationsLoader mLoader;
	
	private boolean mDataLoadPending;
	
	public PlotFragment(Context context, final XYPlot plot)
	{
		if(context == null)
			throw new NullPointerException("'context' parameter cannot be null.");
		
		if(plot == null)
			throw new NullPointerException("'plot' parameter cannot be null.");
		
		this.mContext = context;
		this.mPlot = plot;
		
		this.mGestureDetector = new GestureDetector(context, this);
		
		this.mLoader = new CrimeObservationsLoader();
		
		this.mYearSeriesManager.init();
		this.mMonthSeriesManager.init();
		this.mDaySeriesManager.init();
		this.mHourSeriesManager.init();
		
		this.mTimeSeriesManager = this.mMonthSeriesManager;
		this.mTimeSeriesManager.attach(this.mPlot, this.mLoader, this);
		
		this.mPlot.setRangeValueFormat(new DecimalFormat("0"));
		this.mPlot.setDomainValueFormat(new Format()
		{
			/**
			 * 
			 */
			@Override
			public StringBuffer format(Object xVal, StringBuffer buffer,
					FieldPosition fieldPos) {
				
				return buffer;
			}

			@Override
			public Object parseObject(String arg0, ParsePosition arg1) {
				// TODO Auto-generated method stub
				return null;
			}
			
		});
		
		this.mPlot.setRangeLowerBoundary(0, BoundaryMode.FIXED);
		
		this.tryRedraw(this.getInitialTimeStart(), this.getInitialTimeEnd());
		
		this.mPlot.setOnTouchListener(this);
		this.mGestureDetector.setOnDoubleTapListener(this);
	}
	
	private long getInitialTimeStart()
	{
		Calendar cal = Calendar.getInstance();
		cal.add(Calendar.MONTH, -6);
		return cal.getTimeInMillis();
	}
	
	private long getInitialTimeEnd()
	{
		Calendar cal = Calendar.getInstance();
		cal.add(Calendar.MONTH, -1);
		return cal.getTimeInMillis();
	}
	
	public boolean goBack()
	{
		if(this.mDataLoadPending)
			return true;
		
		if(!this.changeDisplayManagerForWiderTimeRange())
			return false;
		
		long domainCenterValue = this.mMinTime + (this.mMaxTime - this.mMinTime) / 2;
		Pair<Long, Long> centeredDomainBoundaries =
				this.getDomainBoundariesCentered(domainCenterValue, this.getDefaultDomainSizeForCurrentTimeSeriesManager());
		
		this.tryRedraw(centeredDomainBoundaries.first, centeredDomainBoundaries.second);
		
		return true;
	}
	
	private long getDefaultDomainSizeForCurrentTimeSeriesManager()
	{
		return this.mTimeSeriesManager.getMaximumDisplayTimeRange() / 2;
	}
	
	private Pair<Long, Long> getDomainBoundariesCentered(final long domainCenterValue, final long domainSize)
	{
		long leftDomainPartSize = domainSize / 2;
		long rightDomainPartSize = domainSize - leftDomainPartSize;
		
		return new Pair<Long, Long>(domainCenterValue - leftDomainPartSize, domainCenterValue + rightDomainPartSize);
	}
	
	private void tryRedraw(long minTime, long maxTime)
	{
		if(this.mDataLoadPending)
			return;
		
		this.mDataLoadPending = true;
		
		long timeSpanRangeSize = maxTime - minTime;
		
		if(timeSpanRangeSize <= this.mTimeSeriesManager.getMinimumDisplayTimeRange())
		{
			if(!this.changeDisplayManagerForNarrowerTimeRange())
				return;
		}
		else if(timeSpanRangeSize > this.mTimeSeriesManager.getMaximumDisplayTimeRange())
		{
			if(!this.changeDisplayManagerForWiderTimeRange())
				return;
		}
		
		// begin async redrawing of a plot
		this.mTimeSeriesManager.beginRedraw(minTime, maxTime);
	}
	
	private boolean changeDisplayManagerForNarrowerTimeRange()
	{
		TimeSeriesManager newTimeSeriesManager = null;
		
		if(this.mTimeSeriesManager instanceof YearSeriesManager)
		{
			newTimeSeriesManager = this.mMonthSeriesManager;
		}
		else if(this.mTimeSeriesManager instanceof MonthSeriesManager)
		{
			newTimeSeriesManager = this.mDaySeriesManager;
		}
		else if(this.mTimeSeriesManager instanceof DaySeriesManager)
		{
			newTimeSeriesManager = this.mHourSeriesManager;
		}
		
		boolean hadDisplayManagerChanged = newTimeSeriesManager != null;
		if(hadDisplayManagerChanged)
		{
			this.mTimeSeriesManager.detach();
			this.mTimeSeriesManager = newTimeSeriesManager;
			this.mTimeSeriesManager.attach(this.mPlot, this.mLoader, this);
		}
		
		return hadDisplayManagerChanged;
	}
	
	private boolean changeDisplayManagerForWiderTimeRange()
	{
		TimeSeriesManager newTimeSeriesManager = null;
		
		if(this.mTimeSeriesManager instanceof HourSeriesManager)
		{
			newTimeSeriesManager = this.mDaySeriesManager;
		}
		else if(this.mTimeSeriesManager instanceof DaySeriesManager)
		{
			newTimeSeriesManager = this.mMonthSeriesManager;
		}
		else if(this.mTimeSeriesManager instanceof MonthSeriesManager)
		{
			newTimeSeriesManager = this.mYearSeriesManager;
		}
		
		boolean hadDisplayManagerChanged = newTimeSeriesManager != null;
		if(hadDisplayManagerChanged)
		{
			this.mTimeSeriesManager.detach();
			this.mTimeSeriesManager = newTimeSeriesManager;
			this.mTimeSeriesManager.attach(this.mPlot, this.mLoader, this);
		}
		
		return hadDisplayManagerChanged;
	}
	
	private boolean mZooming;
	private double mDistanceBetweenFingers;
	@Override
	public boolean onTouch(View v, MotionEvent event)
	{
		//boolean handled = this.mGestureDetector.onTouchEvent(event);
		boolean handled = false;
		
		switch(event.getAction() & MotionEvent.ACTION_MASK)
		{
			case MotionEvent.ACTION_DOWN:
			{
				this.lastX = event.getX();
				handled = true;
				break;
			}
			
			case MotionEvent.ACTION_MOVE:
			{
				// zooming
//				if(this.mZooming)
//				{
//					double currentFingerDistance = this.getDistanceBetweenFingers(event);
//					this.zoom(this.mDistanceBetweenFingers / currentFingerDistance);
//					this.mDistanceBetweenFingers = currentFingerDistance;
//				}
//				// panning
//				else
//				{
//					float currentX = event.getX();
//					scroll(currentX - this.lastX);
//					this.lastX = currentX;
//				}
//				
//				this.mPlot.redraw();
				
				handled = true;
				break;
			}
			
			case MotionEvent.ACTION_POINTER_UP:
			{
				// zooming
				if(this.mZooming)
				{
					double currentFingerDistance = this.getDistanceBetweenFingers(event);
					
					if(currentFingerDistance != 0.0d)
					{
						double ratio = this.mDistanceBetweenFingers / currentFingerDistance;
						
						if(ratio != 0.0d)
						{
							this.zoom(ratio);
							//this.mDistanceBetweenFingers = currentFingerDistance;
						}
					}
				}
				
				handled = true;
				break;
			}
			
			case MotionEvent.ACTION_UP:
			{
				if(!this.mZooming)
				{
					float currentX = event.getX();
					if(currentX - this.lastX != 0.0f)
					{
						scroll(currentX - this.lastX);
					}
				}
				else
				{
					this.mZooming = false;
				}
				
				handled = true;
				
				break;
			}
			
			case MotionEvent.ACTION_POINTER_DOWN:	
			{
				this.mDistanceBetweenFingers = this.getDistanceBetweenFingers(event);
				if(this.mDistanceBetweenFingers > MIN_FINGER_DISTANCE_FOR_ZOOM)
				{
					this.mZooming = true;
				}
				
				handled = true;
				break;
			}
		}

		return handled;
	}

	private void scroll(float pixelDist)
	{
		float plotPixelWidth = this.mPlot.getWidth();
		double relativeScroll = pixelDist / plotPixelWidth;
		Long domainValueRangeWidth = this.mMaxTime - this.mMinTime;
		long domainValueRangeScroll = -((Double)(relativeScroll * domainValueRangeWidth.doubleValue())).longValue();
		
		long minTime = this.mMinTime + domainValueRangeScroll;
		long maxTime = this.mMaxTime + domainValueRangeScroll;
		
		this.tryRedraw(minTime, maxTime);
	}
	
	private double getDistanceBetweenFingers(MotionEvent event)
	{
		float difX = event.getX(1) - event.getX(0);
		float difY = event.getY(1) - event.getY(0);
		
		return java.lang.Math.sqrt(difX * difX + difY * difY);
	}
	
	private void zoom(double ratio)
	{
		Long domainTimeRangeSize = this.mMaxTime - this.mMinTime;
		long centerTimePoint = this.mMinTime + domainTimeRangeSize / 2;
		long newDomainRangeSize = ((Double)(domainTimeRangeSize.doubleValue() * ratio)).longValue();
		
		long minTime = centerTimePoint - newDomainRangeSize / 2;
		long maxTime = this.mMinTime + newDomainRangeSize;
		
		this.tryRedraw(minTime, maxTime);
	}
	
	@Override
	public void onCrimeObservationsLoaded(CrimeTimeSeriesObservation[] loadedObservations, long minTime, long maxTime)
	{
		this.mDataLoadPending = false;
		
		// note currently loaded values as the currently displayed ones
		this.mMinTime = minTime;
		this.mMaxTime = maxTime;
		
		// set the boundaries
		this.mPlot.setDomainBoundaries(this.mMinTime, this.mMaxTime, BoundaryMode.FIXED);
		
		// redraw the plot
		this.mPlot.redraw();
	}

	@Override
	public boolean onDown(MotionEvent e) {
		// TODO Auto-generated method stub
		return true;
	}

	@Override
	public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
			float velocityY) {
		// TODO Auto-generated method stub
		return false;
	}

	@Override
	public void onLongPress(MotionEvent e) {
		// TODO Auto-generated method stub
		
	}

	@Override
	public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
			float distanceY) {
		// TODO Auto-generated method stub
		return false;
	}

	@Override
	public void onShowPress(MotionEvent e) {
		// TODO Auto-generated method stub
		
	}

	@Override
	public boolean onSingleTapUp(MotionEvent e) {
		// TODO Auto-generated method stub
		return false;
	}
	
	public boolean onDoubleTap(android.view.MotionEvent e)
	{	
		long domainCenterValue = this.mPlot.getXVal(new PointF(e.getX(), e.getY())).longValue();
		Pair<Long, Long> centeredDomainBoundaries =
				this.getDomainBoundariesCentered(domainCenterValue, this.getDefaultDomainSizeForCurrentTimeSeriesManager());
		
		this.tryRedraw(centeredDomainBoundaries.first, centeredDomainBoundaries.second);
		
		return true;
	}

	@Override
	public boolean onDoubleTapEvent(MotionEvent e) {
		// TODO Auto-generated method stub
		return false;
	}

	@Override
	public boolean onSingleTapConfirmed(MotionEvent e) {
		// TODO Auto-generated method stub
		return false;
	}
}

















